/**
 *       Project: ServerLib
 *     Copyright: Copyright (C) 2008, Rob Kennedy
 *   Create Date: 2008-07-15 3:40 PM
 * Modified Date: 2008-07-15 3:40 PM    RK  Added comments
 *      Comments: Ignore SA1633.
 *      
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

namespace ServerLib.Security
{
    using System;
    using System.Security.Cryptography;
    using System.Text;

    /// <summary>
    /// Enumeration holds the encoding method for hash data returned
    /// </summary>
    public enum HashEncodeMethod
    {
        /// <summary>Encode hash in lower-case hex coded string (two characters per byte)</summary>
        HexCode = 0,

        /// <summary>Encode hash as a Base64 encoded string</summary>
        Base64 = 1
    }

    /// --------------------------------------------------------------------
    /// Project	 : ServerLib
    /// Class	 : Hash
    /// Inherits : None.
    /// --------------------------------------------------------------------
    /// <summary>
    /// Hash contains the class interfaces for encryption of passwords
    /// for user accounts as well as computing hashes for general data.
    /// </summary>
    /// <authorname>Rob Kennedy</authorname>
    /// <remarks></remarks>
    /// --------------------------------------------------------------------
    public class Hash
    {
        #region Public Enumerations
        /// <summary>
        /// Enumeration holds the hashing method to use
        /// </summary>
        public enum HashMethod
        {
            /// <summary>Use the MD5 hash algorithm</summary>
            MD5 = 0,

            /// <summary>Use the SHA1 hash algorithm</summary>
            SHA1,

            /// <summary>Use the SHA256 hash algorithm</summary>
            SHA256,

            /// <summary>Use the SHA384 hash algorithm</summary>
            SHA384,

            /// <summary>Use the SHA512 hash algorithm</summary>
            SHA512
        }
        #endregion

        #region Private Properties

        /// <summary>
        /// Contains an array of hash method names available
        /// </summary>
        private string[] methodName = { "MD5", "SHA1", "SHA256", "SHA384", "SHA512" };
        #endregion

        #region Public Constructors
        
        /// <summary>
        /// Creates a new instance of Hash class
        /// </summary>
        public Hash()
        {
        }
        #endregion

        #region Public Methods

        /// <summary>
        /// Compute the hash of a plain text value
        /// </summary>
        /// <param name="plainText">The text to hash</param>
        /// <param name="hashAlgorithm">The hashing algorithm to use</param>
        /// <param name="saltBytes">Hash salt bytes</param>
        /// <returns>An encoded hash string in Base64 format</returns>
        public static string ComputeHash(string plainText, HashMethod hashAlgorithm, string saltBytes)
        {
            return ComputeHash(plainText, hashAlgorithm, saltBytes, HashEncodeMethod.Base64);
        }

        /// <summary>
        /// Compute the hash of a plain text value
        /// </summary>
        /// <param name="plainText">The text to hash</param>
        /// <param name="hashAlgorithm">The hashing algorithm to use</param>
        /// <param name="saltBytes">Hash salt bytes</param>
        /// <param name="method">The encoding method to use on hash data</param>
        /// <returns>An encoded hash string in the specified format</returns>
        public static string ComputeHash(string plainText, HashMethod hashAlgorithm, string saltBytes, HashEncodeMethod method)
        {
            byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);
            byte[] bsaltBytes = Encoding.UTF8.GetBytes(saltBytes);
            byte[] plainTextWithSaltBytes = new byte[plainTextBytes.Length + bsaltBytes.Length + 1]; // +1 is fix for compliance with legacy hashes; dang vb!
            
            for (int i = 0; i < plainTextBytes.Length; i++)
            {
                plainTextWithSaltBytes[i] = plainTextBytes[i];
            }

            for (int i = 0; i < bsaltBytes.Length; i++)
            {
                plainTextWithSaltBytes[plainTextBytes.Length + i] = bsaltBytes[i];
            }

            HashAlgorithm hash;
            
            // Initialize appropriate hashing algorithm class.
            switch (hashAlgorithm)
            {
                case HashMethod.SHA1:
                    hash = new SHA1Managed();
                    break;
                case HashMethod.SHA256:
                    hash = new SHA256Managed();
                    break;
                case HashMethod.SHA384:
                    hash = new SHA384Managed();
                    break;
                case HashMethod.SHA512:
                    hash = new SHA512Managed();
                    break;
                default:
                    hash = new MD5CryptoServiceProvider();
                    break;
            }

            // Compute hash value of our plain text with appended salt.
            byte[] hashBytes = hash.ComputeHash(plainTextWithSaltBytes);
            byte[] hashWithSaltBytes = new byte[hashBytes.Length + bsaltBytes.Length + 1];

            for (int i = 0; i < hashBytes.Length; i++)
            {
                hashWithSaltBytes[i] = hashBytes[i];    
            }

            for (int i = 0; i < bsaltBytes.Length; i++)
            {
                hashWithSaltBytes[hashBytes.Length + i] = bsaltBytes[i];
            }

            string hashValue = string.Empty;
            
            // Convert result into a base64-encoded string.
            if (method == HashEncodeMethod.Base64)
            {
                hashValue = Convert.ToBase64String(hashWithSaltBytes);
            }
            else
            {
                hashValue = BytesToHex(hashWithSaltBytes);
            }

            // Return the result.
            return hashValue;
        }
       
        /// <summary>
        /// Verifies a hash value is correct for a specified plaintext value
        /// </summary>
        /// <param name="plainText">plain text to verify with</param>
        /// <param name="hashAlgorithm">The hashing algorithm to use</param>
        /// <param name="hashValue">The base64 encoded hash value to verify</param>
        /// <returns>Returns true if the hash code is valid</returns>
        public static bool VerifyHash(string plainText, HashMethod hashAlgorithm, string hashValue)
        {
            return VerifyHash(plainText, hashAlgorithm, hashValue, HashEncodeMethod.Base64);
        }
        
        /// <summary>
        /// Verifies a hash value is correct for a specified plaintext value
        /// </summary>
        /// <param name="plainText">plain text to verify with</param>
        /// <param name="hashAlgorithm">The hashing algorithm to use</param>
        /// <param name="hashValue">The hash value to verify</param>
        /// <param name="method">The encoding method to use on hash data</param>
        /// <returns>Returns true if the hash code is valid</returns>
        public static bool VerifyHash(string plainText, HashMethod hashAlgorithm, string hashValue, HashEncodeMethod method)
        {
            byte[] hashWithSaltBytes;

            if (method == HashEncodeMethod.Base64)
            {
                hashWithSaltBytes = Convert.FromBase64String(hashValue);
            }
            else
            {
                hashWithSaltBytes = HexToBytes(hashValue);
            }

            // Convert base64-encoded hash value into a byte array.
            
            // We must know size of hash (without salt).
            int hashSizeInBits, hashSizeInBytes;

            // Size of hash is based on the specified algorithm.
            switch (hashAlgorithm)
            {
                case HashMethod.SHA1:
                    hashSizeInBits = 160;
                    break;
                case HashMethod.SHA256:
                    hashSizeInBits = 256;
                    break;
                case HashMethod.SHA384:
                    hashSizeInBits = 384;
                    break;
                case HashMethod.SHA512:
                    hashSizeInBits = 512;
                    break;
                default:
                    hashSizeInBits = 128;
                    break;
            }

            // Convert size of hash from bits to bytes.
            hashSizeInBytes = hashSizeInBits / 8;

            bool resultValue = false;

            // Make sure that the specified hash value is long enough.
            if (hashWithSaltBytes.Length >= hashSizeInBytes)
            {
                // Allocate array to hold original salt bytes retrieved from hash.
                byte[] saltBytes = new byte[hashWithSaltBytes.Length - hashSizeInBytes];
                
                // Copy salt from the end of the hash to the new array.
                for (int i = 0; i <= saltBytes.Length - 1; i++)
                {
                    saltBytes[i] = hashWithSaltBytes[hashSizeInBytes + i];
                }

                // Compute a new hash string.
                string expectedHashString = ComputeHash(plainText, hashAlgorithm, Encoding.UTF8.GetString(saltBytes), method);

                // If the computed hash matches the specified hash,  
                // the plain text value must be correct.
                resultValue = (hashValue == expectedHashString);
            }

            return resultValue;
        }

        /// <summary>
        /// Quickly returns an MD5 encoded hash value
        /// </summary>
        /// <param name="bytesIn">The text string to MD5 hash encode</param>
        /// <returns>A base64 encoded MD5 hash string</returns>
        public static string QuickMD5(string bytesIn)
        {
            return QuickMD5(bytesIn, HashEncodeMethod.Base64);
        }

        /// <summary>
        /// Quickly returns an MD5 encoded hash value
        /// </summary>
        /// <param name="bytesIn">The text string to MD5 encode</param>
        /// <param name="method">The encoding method to use on hash data</param>
        /// <returns>Returns an MD5 hash string encoded using the specified method</returns>
        public static string QuickMD5(string bytesIn, HashEncodeMethod method)
        {
            byte[] data = System.Text.Encoding.ASCII.GetBytes(bytesIn);
            
            // This is one implementation of the abstract class MD5.
            MD5 md5 = new MD5CryptoServiceProvider();

            string resultValue = string.Empty;

            if (method == HashEncodeMethod.Base64)
            {
                resultValue = Convert.ToBase64String(md5.ComputeHash(data));
            }
            else
            {
                resultValue = BytesToHex(md5.ComputeHash(data));
            }

            return resultValue;
        }

        /// <summary>
        /// Quickly returns a strong SHA512 encoded hash value
        /// </summary>
        /// <param name="bytesIn">The text string to SHA encode</param>
        /// <returns>A base64 encoded SHA hash string</returns>
        public static string QuickSHA512(string bytesIn)
        {
            return QuickSHA512(bytesIn, HashEncodeMethod.Base64);
        }

        /// <summary>
        /// Quickly returns a strong SHA512 encoded hash value
        /// </summary>
        /// <param name="bytesIn">The text string to SHA encode</param>
        /// <param name="method">The encoding method to use on hash data</param>
        /// <returns>Returns a SHA512 hash string encoded using the specified method</returns>
        public static string QuickSHA512(string bytesIn, HashEncodeMethod method)
        {
            byte[] data = System.Text.Encoding.ASCII.GetBytes(bytesIn);
            
            SHA512 sha512 = new SHA512Managed();

            string resultValue = string.Empty;
            if (method == HashEncodeMethod.Base64)
            {
                resultValue = Convert.ToBase64String(sha512.ComputeHash(data));
            }
            else
            {
                resultValue = BytesToHex(sha512.ComputeHash(data));
            }

            return resultValue;
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Converts a hex encoded string to a binary byte array
        /// </summary>
        /// <param name="hashValue">hex value encoded string</param>
        /// <returns>byte array of decoded binary data</returns>
        private static byte[] HexToBytes(string hashValue)
        {
            byte[] outBytes = new byte[hashValue.Length / 2];
            string hexVal;
            int n = 0;

            for (int i = 0; i < hashValue.Length; i += 2)
            {
                hexVal = hashValue.Substring(i, 2);
                outBytes[n] = Byte.Parse(hexVal, System.Globalization.NumberStyles.HexNumber);
                n++;
            }

            return outBytes;
        }

        /// <summary>
        /// Converts a binary byte array to a hex encoded string
        /// </summary>
        /// <param name="bytesIn">byte array of binary data</param>
        /// <returns>Returns a hex encoded string</returns>
        private static string BytesToHex(byte[] bytesIn)
        {
            string outBuf = string.Empty;

            for (int i = 0; i < bytesIn.Length; i++)
            {
                outBuf += String.Format("{0:x2}", bytesIn[i]);
            }

            return outBuf;
        }
        #endregion
    }
}